package com.fhc.ble


import android.app.Application
import androidx.lifecycle.AndroidViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.filterNot
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onEmpty
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withTimeout
import no.nordicsemi.kotlin.ble.client.android.CentralManager
import no.nordicsemi.kotlin.ble.client.android.ConnectionPriority
import no.nordicsemi.kotlin.ble.client.android.Peripheral
import no.nordicsemi.kotlin.ble.client.android.preview.PreviewPeripheral
import no.nordicsemi.kotlin.ble.client.distinctByPeripheral
import no.nordicsemi.kotlin.ble.core.ConnectionState
import no.nordicsemi.kotlin.ble.core.Phy
import no.nordicsemi.kotlin.ble.core.PhyInUse
import no.nordicsemi.kotlin.ble.core.WriteType
import no.nordicsemi.kotlin.ble.core.util.fromShortUuid
import timber.log.Timber
import javax.inject.Inject
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
import kotlin.uuid.ExperimentalUuidApi
import kotlin.uuid.Uuid
import no.nordicsemi.kotlin.ble.client.CentralManager.ScanFilterScope

@HiltViewModel
class ScannerViewModel @Inject constructor(
    private val centralManager: CentralManager,
    // We're not using ViewModelScope. For test purposes it's better to create a custom Scope,
    // also connected to the ViewModel lifecycle, but which can be replaced in tests.
    private val scope: CoroutineScope,
    app : Application
) : AndroidViewModel(app) {
    val state = centralManager.state

    private val _devices: MutableStateFlow<List<Peripheral>> = MutableStateFlow(
        mutableListOf()
    )

//    listOf(
//    PreviewPeripheral(scope, phy = PhyInUse(txPhy = Phy.PHY_LE_1M, rxPhy = Phy.PHY_LE_2M))
//    .apply {
//        // Track state of each peripheral.
//        // Note, that the states are observed using the view model scope, even when the
//        // device isn't connected.
//        observePeripheralState(this, scope)
//        // Track bond state of each peripheral.
//        observeBondState(this, scope)
//    }
//    )

    val devices = _devices.asStateFlow()

    private val _isScanning: MutableStateFlow<Boolean> = MutableStateFlow(false)
    val isScanning: StateFlow<Boolean> = _isScanning.asStateFlow()

    private var connectionScopeMap = mutableMapOf<Peripheral, CoroutineScope>()

    private var scanningJob: Job? = null


    @OptIn(ExperimentalUuidApi::class)
    fun onScanRequested() {
        _isScanning.update { true }
        scanningJob = centralManager
            .scan(5000.milliseconds) {
//                Any {
//                    ManufacturerData(0x0059)
//                    ServiceUuid(Uuid.fromShortUuid(0x1809))
//                }
//                Any {
//                    Name( Regex(".+"))  //表示匹配任意字符（除换行符外），至少出现一次。即确保字符串不为空。
                    Name(Regex("\\S+"))// 匹配去除首尾空格后非空的设备名称
//                    Name(Regex("Mesh.*"))
//                }
            }
            .distinctByPeripheral()
            .map {
                it.peripheral
            }
            .filterNot { _devices.value.contains(it) }
            //.distinct()
            .onEach { newPeripheral ->
                Timber.i("Found new device: ${newPeripheral.name} (${newPeripheral.address})")
                _devices.update { devices.value + newPeripheral }
            }
            .onEach { peripheral ->
                // Track state of each peripheral.
                // Note, that the states are observed using the view model scope, even when the
                // device isn't connected.
                observePeripheralState(peripheral, scope)
                // Track bond state of each peripheral.
                observeBondState(peripheral, scope)
            }
            .catch { t ->
                Timber.e("Scan failed: $t")
            }
            .onCompletion {
                _isScanning.update { false }
            }
            .launchIn(scope)
    }

    fun onStopScanRequested() {
        scanningJob?.cancel()
    }

    fun onPeripheralSelected(peripheral: Peripheral) {
        // If the connection scope exists for the given peripheral, that means we're connected
        // and the user initiated disconnection.
        val connectionScope = connectionScopeMap[peripheral]
        connectionScope?.launch {
            Timber.v("Disconnecting from ${peripheral.name}...")
            try {
                peripheral.disconnect()
                Timber.i("Disconnected from ${peripheral.name}!")
            } catch (e: Exception) {
                Timber.e(e, "Disconnect failed")
            }
        } ?: run {
            // Otherwise, create a new connection scope that will handle all events until we are
            // done with the device.
            connectionScopeMap[peripheral] = CoroutineScope(context = Dispatchers.IO)
                .apply {
                    launch {
                        try {
                            withTimeout(5000) {
                                connect(peripheral, false)
                            }

                            // The first time the app connects to the peripheral it needs to initiate
                            // observers for various parameters.
                            // The observers will get cancelled when the connection scope gets cancelled,
                            // that is when the device is manually disconnected in case of auto connect,
                            // or disconnects for any reason when auto connect was false.
                            observerPhy(peripheral, this)
                            observeConnectionParameters(peripheral, this)
                            observerServices(peripheral, this)
                        } catch (e: Exception) {
                            Timber.e(e, "Connection attempt failed")
                        }
                    }
                }
        }
    }

    fun onBondRequested(peripheral: Peripheral) {
        scope.launch {
            try {
                Timber.i("Bonding with ${peripheral.name}...")
                peripheral.createBond()
                Timber.i("Bonding successful")
            } catch (e: Exception) {
                Timber.e(e, "Bonding failed")
            }
        }
    }

    fun onRemoveBondRequested(peripheral: Peripheral) {
        scope.launch {
            try {
                Timber.i("Removing bond information...")
                peripheral.removeBond()
                Timber.i("Bond removed")
            } catch (e: Exception) {
                Timber.e(e, "Removing bond failed")
            }
        }
    }

    fun onClearCacheRequested(peripheral: Peripheral) {
        scope.launch {
            try {
                Timber.i("Clearing cache...")
                peripheral.refreshCache()
                Timber.i("Cache cleared")
            } catch (e: Exception) {
                Timber.e(e, "Clearing cache failed")
            }
        }
    }

    override fun onCleared() {
        super.onCleared()
        centralManager.close()
    }

    // ---- Implementation ----

    private suspend fun connect(peripheral: Peripheral, autoConnect: Boolean) {
        Timber.v("Connecting to ${peripheral.name}...")
        centralManager.connect(
            peripheral = peripheral,
            options = if (autoConnect) {
                CentralManager.ConnectionOptions.AutoConnect
            } else {
                CentralManager.ConnectionOptions.Direct(
                    timeout = 3.seconds,
                    retry = 2,
                    retryDelay = 1.seconds,
                    Phy.PHY_LE_2M,
                )
            },
        )
        Timber.i("Connected to ${peripheral.name}!")
    }

    private suspend fun initiateConnection(peripheral: Peripheral) {
        try {
            // Request MTU
            peripheral.requestHighestValueLength()

            // Check maximum write length
            val writeType = WriteType.WITHOUT_RESPONSE
            val length = peripheral.maximumWriteValueLength(writeType)
            Timber.i("Maximum write length for $writeType: $length")

            // Read RSSI
            val rssi = peripheral.readRssi()
            Timber.i("RSSI: $rssi dBm")

            // Read PHY
            val phyInUse = peripheral.readPhy()
            Timber.i("PHY in use: $phyInUse")

            // Request connection priority
            val newConnectionParameters = peripheral.requestConnectionPriority(ConnectionPriority.HIGH)
            Timber.i("Connection priority changed to HIGH")
            Timber.i("New connection parameters: $newConnectionParameters")
        } catch (e: Exception) {
            Timber.e(e, "OMG!")
        }
    }

    private fun observerPhy(peripheral: Peripheral, scope: CoroutineScope) {
        peripheral.phy
            .onEach {
                Timber.i("PHY changed to: $it")
            }
            .onEmpty {
                Timber.w("PHY didn't change")
            }
            .onCompletion {
                Timber.d("PHY collection completed")
            }
            .launchIn(scope)
    }

    private fun observeConnectionParameters(peripheral: Peripheral, scope: CoroutineScope) {
        peripheral.connectionParameters
            .onEach {
                Timber.i("Connection parameters changed to: $it")
            }
            .onEmpty {
                Timber.w("Connection parameters didn't change")
            }
            .onCompletion {
                Timber.d("Connection parameters collection completed")
            }
            .launchIn(scope)
    }

    @OptIn(ExperimentalStdlibApi::class, ExperimentalUuidApi::class)
    private fun observerServices(peripheral: Peripheral, scope: CoroutineScope) {
        peripheral.services()
            .onEach {
                Timber.i("Services changed: $it")

                // Read values of all characteristics.
                it.forEach { remoteService ->
                    remoteService.characteristics.forEach { remoteCharacteristic ->
                        try {
                            val value = remoteCharacteristic.read()
                            Timber.i("Value of ${remoteCharacteristic.uuid}: 0x${value.toHexString()}")
                        } catch (e: Exception) {
                            Timber.e("Failed to read ${remoteCharacteristic.uuid}: ${e.message}")
                        }
                    }
                }

                it.forEach { remoteService ->
                    remoteService.characteristics.forEach { remoteCharacteristic ->
                        try {
                            Timber.w("Subscribing to ${remoteCharacteristic.uuid}...")
                            remoteCharacteristic.subscribe()
                                .onEach { newValue ->
                                    Timber.i("Value of ${remoteCharacteristic.uuid} changed: 0x${newValue.toHexString()}")
                                }
                                .onEmpty {
                                    Timber.w("No updates from ${remoteCharacteristic.uuid}")
                                }
                                .onCompletion {
                                    Timber.d("Stopped observing updates from ${remoteCharacteristic.uuid}")
                                }
                                .launchIn(scope)
                            Timber.i("Notifications for ${remoteCharacteristic.uuid} are now ${if (remoteCharacteristic.isNotifying) "enabled" else "disabled"}")
                        } catch (e: Exception) {
                            Timber.e("Failed to subscribe to ${remoteCharacteristic.uuid}: ${e.message}")
                        }
                    }
                }
            }
            .onEmpty {
                Timber.w("No services found")
            }
            .onCompletion {
                Timber.d("Service collection completed")
            }
            .launchIn(scope)
    }

    private fun observePeripheralState(peripheral: Peripheral, scope: CoroutineScope) {
        peripheral.state
            .onEach {
                Timber.i("State of $peripheral: $it")

                // Each time a connection changes, handle the new state
                when (it) {
                    is ConnectionState.Connected -> {
                        connectionScopeMap[peripheral]?.launch {
                            initiateConnection(peripheral)
                        }
                    }

                    is ConnectionState.Closed -> {
                        // Just for testing, wait with cancelling the scope to get all the logs.
                        delay(500)
                        // Cancel connection scope, so that previously launched jobs are cancelled.
                        connectionScopeMap.remove(peripheral)?.cancel()
                    }

                    else -> { /* Ignore */ }
                }
            }
            .onCompletion {
                Timber.d("State collection for $peripheral completed")
            }
            .launchIn(scope)
    }

    private fun observeBondState(peripheral: Peripheral, scope: CoroutineScope) {
        peripheral.bondState
            .onEach {
                Timber.i("Bond state of $peripheral: $it")
            }
            .onCompletion {
                Timber.d("Bond state collection for $peripheral completed")
            }
            .launchIn(scope)
    }
}